#!/usr/bin/python3
# -*- encoding=utf8 -*-

import re
import time
import sys
import subprocess
import platform
import os
import logging.handlers
import logging
import argparse
import locale
from typing import List, Tuple, Optional, Set
ABOUT = """
# Author: Yuwenhai <y.wh@foxmail.com>
# Date:   2022-04-18
# Page:   www.whlws.cn
# Gitee:  https://gitee.com/yuwenhai/device-tree-tools
"""


Version = "1.5"
Author = "Yuwenhai <y.wh@foxmail.com>"
History = """
#原始版本参考自:
#https://blog.csdn.net/daoshuti/article/details/81568519

#变更记录:
#v0.1
#1)基于参考版本,使用python2实现基本功能。

#v0.2
#1)优化显示逻辑
#2)BUG修复

#v0.3
#1)增加搜索功能
#2)增加路径显示选项(相对路径,绝对路径)
#3)增加默认配置文件设置
#4)BUG修复
#5)完善help菜单

#v0.4
#1)给搜索到的关键子增加颜色效果
#2)修复BUG

#v0.5
#1)增加多个DevicesTree根结点的支持
#2)给dtv.conf增加注释的支持,忽略#开头的根节点
#3)BUG修复

#v0.6
#1)使用Python3重构工具
#2)增加正则表达式搜索逻辑,可以输入正则表达式搜索内容

#0.7
#1)优化命令行传参逻辑
#2)修复配置文件不存在时候的报错
#3)增加搜索配置文件的逻辑,从当前目录下没发现配置文件的话,逐层向外搜索
#4)命令行传参的时候,可以指定配置文件,也可以指定设备树文件

#0.8
#1)优化变量名称
#2)增加显示配置文件命令
#3)增加显示使用的设备树命令

#0.9
#1)优化对 // 注释的处理逻辑

#1.0(20230217)
#1)在当前执行命令的位置,显示搜索功能文件的相对路径
#2)修改显示的路径,从绝对路径修改为相对当前命令行的相对路径

#1.1(20230807)
#1)优化搜索功能,默认大小写不敏感

#1.2(20230808)
#1)优化搜索功能,增加批量搜索,使用分隔符分割字符串:"str1|str2|123"
#2)优化搜索功能,增加"-x cc"选项来进行大小写敏感的搜索
#3)优化配置文件的读取逻辑,在配置文件为相对路径时,可以在子目录中正常处理

#1.3(20231205)
#1)增加-x ss选项,可以展示当前搜索结果所属的结构

#1.4(20231207)
#1)增加-ls --linestyle选项,可以使用vim或vscode风格显示搜索结果的行号
#
#1.5(20250825)
#1)修复正则分支中未定义变量的BUG,稳定显示所属名称
#2)增强注释剥离,正确处理多行/* ... */注释,提升搜索准确性
#3)修复#include仅匹配.dts的BUG,现支持.dtsi/.dts
#4)修复函数默认参数为可变对象的BUG,避免状态泄漏
#5)改进文件读写编码与子进程解码,提升跨平台兼容性
#6)CLI支持--linestyle别名,并改进Debug日志输出
"""

hidePath = False
rootPath = os.path.abspath(__file__)
findString = [""]
findWithRegex = False
findCareCase = False
showAll = False
noColor = False
showConfig = False
showRoot = False
configLoading = []
showStock = False

linestyle = "vim"

LOG_FILENAME = '/tmp/device_tree.log'
logger = logging.getLogger()
current_dir = os.getcwd()


def set_logger(debug: bool = False) -> None:
    logger.setLevel(logging.DEBUG if debug else logging.INFO)

    # Remove any existing handlers to avoid duplicate logs
    for handler in list(logger.handlers):
        logger.removeHandler(handler)

    formatter_console = logging.Formatter('%(message)s')
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.DEBUG if debug else logging.ERROR)
    console_handler.setFormatter(formatter_console)
    logger.addHandler(console_handler)

    file_handler = logging.handlers.RotatingFileHandler(
        LOG_FILENAME, maxBytes=10485760, backupCount=5)
    file_handler.setLevel(logging.DEBUG)
    formatter_file = logging.Formatter(
        '[line:%(lineno)d] - %(levelname)s: %(message)s')
    file_handler.setFormatter(formatter_file)
    logger.addHandler(file_handler)


set_logger()


def find_substrings(main_string: str, sub_strings: List[str]) -> List[str]:
    pattern = '|'.join(re.escape(sub) for sub in sub_strings)
    matches = re.finditer(pattern, main_string,
                          0 if findCareCase else re.IGNORECASE)

    found_substrings: List[str] = []
    for match in matches:
        found_substrings.append(match.group())

    return found_substrings


def sanitize_display_path(path_text: str) -> str:
    try:
        sanitized = path_text.replace("/memory/firmware/reserved-memory/", "/")
        sanitized = re.sub(r'//+', '/', sanitized)
        return sanitized
    except Exception:
        return path_text


def replace_leading_tabs(line_text: str, spaces_per_tab: int = 2) -> str:
    try:
        return re.sub(r'^(\t+)', lambda m: ' ' * (len(m.group(1)) * spaces_per_tab), line_text)
    except Exception:
        return line_text


def run_cmd(cmd_list: List[str]) -> Tuple[Optional[List[str]], Optional[str]]:
    logger.debug("run:"+str(cmd_list))
    if (not isinstance(cmd_list, list)):
        raise ValueError("cmd List needs to be a list type [%s]" % cmd_list)

    try:
        run_proc = subprocess.Popen(
            cmd_list,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            shell=platform.system().lower() == 'windows')
        (output, error) = run_proc.communicate()

        is_windows = platform.system().lower() == 'windows'
        preferred_encoding = 'gb2312' if is_windows else (locale.getpreferredencoding(False) or 'utf-8')
        try:
            output = output.decode(preferred_encoding)
        except Exception:
            output = output.decode('utf-8', errors='ignore')
        try:
            error = error.decode(preferred_encoding)
        except Exception:
            error = error.decode('utf-8', errors='ignore')

        if len(output) == 0:
            output = None
        else:
            output = [x.strip() for x in output.split('\n')
                      if len(x.strip()) > 0]
        return output, error
    except FileNotFoundError:
        logger.error("ERROR: Cmd Error [%s]" % (str(cmd_list)))
        return None, "FileNotFoundError"
    except TypeError:
        logger.error("ERROR: Cmd Error [%s]" % (str(cmd_list)))
        return None, "TypeError"
    except Exception as err:
        logger.error("ERROR: %s", err)
        return None, str(err)


class DeviceTreeMap(object):
    def __init__(self, name, parent=None, level=0):
        self.name = os.path.abspath(name)
        self.level = level
        self.parent = parent
        self.child = []
        self.find = []
        self.lastone = False
        self.tmpline = ""

    def tree_print(self, log, end=''):
        if end != '' and end != "\a":
            self.tmpline = self.tmpline + log + end
            # logger.error(self.tmpline)
            return
        else:
            self.tmpline = self.tmpline + log

        self.tmpline = self.tmpline.replace("\n", "")
        logger.error(self.tmpline)
        self.tmpline = ""

    def hasFindedChiled(self):
        if (len(self.find) > 0):
            return True
        if len(self.child) > 0:
            res = False
            for achild in self.child:
                res = res or achild.hasFindedChiled()
                if res:
                    break
            return res
        else:
            return False

    def printChild(self, lastone=False):
        n = 0
        self.lastone = lastone
        relative_path = ""
        if len(findString[0]) == 0 or (len(self.find) > 0 or showAll) or\
                (not showAll and self.hasFindedChiled()):
            relative_path = os.path.relpath(self.name,current_dir)
            if not self.level == 0:
                if self.level > 1:
                    getparent = self
                    lastonelist = []
                    while getparent.level > 1:
                        if not getparent.parent.lastone:
                            if getparent.level != n+1:
                                lastonelist.append('│')
                        else:
                            if getparent.level != n+1:
                                lastonelist.append(' ')

                        getparent = getparent.parent
                    self.tree_print(' '.join(lastonelist[::-1]), end=' ')

                if lastone:
                    if hidePath:
                        self.tree_print(
                            "└─ " + self.name.replace(rootPath, '.'))
                    else:
                        self.tree_print("└─ " + relative_path)
                else:
                    if hidePath:
                        self.tree_print(
                            "├─ " + self.name.replace(rootPath, '.'))
                    else:
                        self.tree_print("├─ " + relative_path)
            else:
                if hidePath:
                    self.tree_print("In " + rootPath)
                    self.tree_print("" + self.name.replace(rootPath, '.'))
                else:
                    self.tree_print("" + relative_path)

        if len(findString[0]) >= 1 and len(self.find) > 0:
            for findlist in self.find:
                if self.level > 1:
                    getparent = self
                    lastonelist = []
                    while getparent.level > 1:
                        if not getparent.parent.lastone:
                            if getparent.level != n+1:
                                lastonelist.append('│')
                        else:
                            if getparent.level != n+1:
                                lastonelist.append(' ')

                        getparent = getparent.parent
                    self.tree_print(' '.join(lastonelist[::-1]), end=' ')
                    if lastone:
                        self.tree_print("  │", end=' ')
                    else:
                        self.tree_print("│ │", end=' ')

                elif self.level == 1:
                    if lastone:
                        self.tree_print("  │", end=' ')
                    else:
                        self.tree_print("│ │", end=' ')
                else:
                    if lastone:
                        self.tree_print("  │", end=' ')
                    else:
                        self.tree_print("│", end=' ')

                # self.tree_print(str(findlist[0]), end=':')
                filepath = findlist[0]
                relative_path = os.path.relpath(filepath,current_dir)

                if "vim" in linestyle:
                    self.tree_print(str(relative_path), end='+')
                else:
                    self.tree_print(str(relative_path), end=':')

                if noColor:
                    self.tree_print("%04d"%(findlist[1]), end=' ')
                else:
                    self.tree_print("\033[0;35m"+"%04d"%(findlist[1]) +
                                    "\033[0m ", end=' ')

                if not findWithRegex:
                    # base content
                    printstr = replace_leading_tabs(findlist[2])
                    if noColor:
                        try:
                            stock_path = findlist[4] if len(findlist) >= 5 else None
                            if stock_path:
                                printstr += "\t[" + sanitize_display_path(stock_path) + "]"
                            else:
                                printstr += "\t[" + sanitize_display_path("".join(findlist[3][0])) + "]"
                        except Exception:
                            pass
                    else:
                        found_substrings = find_substrings(
                            printstr, findString)
                        if found_substrings:
                            for substring in found_substrings:
                                printstr = printstr.replace(
                                    substring, '\033[0;31m%s\033[0m' % substring)
                        else:
                            pass

                        if showStock:
                            try:
                                stock_path = findlist[4] if len(findlist) >= 5 else None
                                if stock_path:
                                    printstr += "\t\033[0;36m[" + sanitize_display_path(stock_path) + "]\033[0m"
                                else:
                                    printstr += "\t\033[0;36m[" + sanitize_display_path("".join(findlist[3][0])) + "]\033[0m"
                            except Exception:
                                pass
                    # annotate deleted-node
                    is_delete_line = '/delete-node/' in findlist[2]
                    is_deleted_ctx = False
                    if len(findlist) >= 5:
                        try:
                            is_deleted_ctx = self.path_is_deleted(findlist[4])
                        except Exception:
                            is_deleted_ctx = False
                    if is_delete_line or is_deleted_ctx:
                        if noColor:
                            printstr += " (delete-node)"
                        else:
                            printstr += " \033[97;41m(delete-node)\033[0m"

                    self.tree_print(printstr, end='')

                else:
                    pattern = re.compile(r'%s' % (findString),0 if findCareCase else re.IGNORECASE)   # 查找数字
                    linetext = replace_leading_tabs(findlist[2])
                    result = pattern.findall(linetext)

                    #List去重
                    result2 = []
                    for i in result:
                        if i not in result2:
                            result2.append(i)

                    for word in result2:
                        if noColor:
                            pass
                        else:
                            linetext = linetext.replace(
                                word, '\033[0;34m%s\033[0m' % word)
                        pass
                    if showStock:
                        try:
                            stock_path = findlist[4] if len(findlist) >= 5 else None
                            if noColor:
                                if stock_path:
                                    linetext += "\t[" + sanitize_display_path(stock_path) + "]"
                                else:
                                    linetext += "\t[" + sanitize_display_path("".join(findlist[3][0])) + "]"
                            else:
                                if stock_path:
                                    linetext += "\t\033[0;36m[" + sanitize_display_path(stock_path) + "]\033[0m"
                                else:
                                    linetext += "\t\033[0;36m[" + sanitize_display_path("".join(findlist[3][0])) + "]\033[0m"
                        except Exception:
                            pass
                    # annotate deleted-node
                    is_delete_line = '/delete-node/' in findlist[2]
                    is_deleted_ctx = False
                    if len(findlist) >= 5:
                        try:
                            is_deleted_ctx = self.path_is_deleted(findlist[4])
                        except Exception:
                            is_deleted_ctx = False
                    if is_delete_line or is_deleted_ctx:
                        if noColor:
                            linetext += " (delete-node)"
                        else:
                            linetext += " \033[97;41m(delete-node)\033[0m"
                    self.tree_print(linetext, end='')

        n = 0
        while len(self.child) > n:
            self.child[n].printChild(len(self.child) == n+1)
            n = n + 1

    def printMap(self):
        self.printChild(False)

    def createTreeMap(self, childLevel, stock_inherit: Optional[list] = None, visited: Optional[Set[str]] = None, ctx: Optional[dict] = None, path_stack: Optional[list] = None):
        theChild = None
        linenum = 0
        stock = []
        if stock_inherit is None:
            stock_inherit = []
        if visited is None:
            visited = set()
        if ctx is None:
            ctx = {"deleted_paths": set(), "deleted_labels": set(), "label_to_path": {}}
        if path_stack is None:
            path_stack = []
        file_abs = os.path.abspath(self.name)
        if file_abs in visited:
            return ctx
        visited.add(file_abs)

        in_block_comment = False
        try:
            fd = open(self.name, encoding='utf-8', errors='ignore')
        except Exception as e:
            logger.error(["忽略无法打开文件:", self.name, str(e)])
            return ctx
        for raw_line in fd:
            linenum += 1
            line = raw_line

            # 处理多行块注释
            if in_block_comment:
                end_idx = line.find("*/")
                if end_idx == -1:
                    continue
                line = line[end_idx+2:]
                in_block_comment = False
            while True:
                start_idx = line.find("/*")
                if start_idx == -1:
                    break
                end_idx = line.find("*/", start_idx+2)
                if end_idx == -1:
                    line = line[:start_idx]
                    in_block_comment = True
                    break
                line = line[:start_idx] + line[end_idx+2:]

            # 去除单行注释
            if '//' in line:
                line = re.sub(r"(\/\/.*)", "", line)

            # 解析节点开始，记录label与路径
            if " {" in line:
                stock.append(line.replace(" ", "").replace(
                    '\t', "").replace("&", "").replace("{", "::"))
                stock_inherit.append(line.replace(" ", "").replace(
                    '\t', "").replace("&", "").replace("{", "::"))
                try:
                    mnode = re.search(r'^\s*(?:(?P<label>[A-Za-z_][A-Za-z0-9_]*):\s*)?(?P<name>[^\s{]+)\s*\{', raw_line)
                    node_label = None
                    node_name = None
                    if mnode:
                        node_label = mnode.group('label')
                        node_name = mnode.group('name')
                    # 计算当前路径
                    if node_name is not None:
                        parent_path = '/' if len(path_stack) == 0 else path_stack[-1]
                        if node_name == '/':
                            current_path = '/'
                        else:
                            if parent_path.endswith('/'):
                                current_path = parent_path + node_name
                            elif parent_path == '/':
                                current_path = '/' + node_name
                            else:
                                current_path = parent_path + '/' + node_name
                        path_stack.append(current_path)
                        if node_label:
                            ctx["label_to_path"][node_label] = current_path
                except Exception:
                    pass
            elif "};" in line:
                stock = stock[:-1]
                stock_inherit = stock_inherit[:-1]
                if len(path_stack) > 0:
                    path_stack = path_stack[:-1]

            # 解析 /delete-node/ 指令
            if "/delete-node/" in line:
                try:
                    mdel = re.search(r'/delete-node/\s*([^;\s]+)\s*;', line)
                    if mdel:
                        target = mdel.group(1).strip()
                        if target.startswith('&'):
                            ctx["deleted_labels"].add(target[1:])
                        else:
                            if not target.startswith('/'):
                                # 规范为绝对路径
                                target = '/' + target
                            ctx["deleted_paths"].add(target)
                except Exception:
                    pass

            if not findWithRegex:
                if len(findString[0]) >= 1 and find_substrings(line, findString):
                    # 记录当前节点路径以支持后续删除过滤
                    current_path = path_stack[-1] if len(path_stack) else '/'
                    self.find.append((self.name, linenum, raw_line, (stock,stock_inherit), current_path))
            else:
                pattern = re.compile(r'%s' % (findString),
                                     0 if findCareCase else re.IGNORECASE)
                result = pattern.findall(line)
                if len(result):
                    current_path = path_stack[-1] if len(path_stack) else '/'
                    self.find.append((self.name, linenum, raw_line, (stock,stock_inherit), current_path))

            if "#include" in line and (".dtsi" in line or ".dts" in line):
                m = re.search(r'#include\s+["<](.*?)[">"]', line)
                if m:
                    dtsiName = m.group(1)

                    path = os.path.abspath(self.name)
                    path = path[:path.rfind('/')]
                    dtsiName = path + '/' + \
                        dtsiName.replace("<", "").replace(
                            ">", "").replace("\"", "")
                    theChild = DeviceTreeMap(dtsiName, self, childLevel)
                    if not dtsiName.endswith(".h") and os.path.exists(dtsiName):
                        self.child.append(theChild)
                        theChild.createTreeMap(childLevel+1, stock_inherit, visited, ctx, list(path_stack))
        fd.close()
        return ctx

    def prune_deleted(self, ctx: dict) -> None:
        # 解析被删除路径集合并存放于实例，供打印阶段标注
        deleted_paths = set(ctx.get("deleted_paths", set()))
        label_to_path = ctx.get("label_to_path", {})
        for lbl in ctx.get("deleted_labels", set()):
            p = label_to_path.get(lbl)
            if p:
                deleted_paths.add(p)

        self.deleted_paths_ctx = deleted_paths

        # 递归子节点，传递上下文
        for ch in self.child:
            ch.deleted_paths_ctx = deleted_paths
            ch.prune_deleted(ctx)

    def path_is_deleted(self, p: str) -> bool:
        dset = getattr(self, 'deleted_paths_ctx', set())
        for dp in dset:
            if p == dp:
                return True
            if dp != '/' and (p.startswith(dp + '/') or p == dp):
                return True
        return False

def parse_list(arg_value):
    if "|" in arg_value:
        return arg_value.split('|')
    else:
        return [arg_value]


def parse_bool(val: str) -> Optional[bool]:
    s = str(val).strip().lower() if val is not None else None
    if s in ("1", "true", "yes", "on", "y"):  # noqa: E712
        return True
    if s in ("0", "false", "no", "off", "n"):  # noqa: E712
        return False
    return None


def parse_args():
    parser = argparse.ArgumentParser(
        add_help=False, description='设备树查看器(%s)' % (Version), usage="从设置的根节点预览或搜索指定关键字")

    parser.add_argument("-s", "-set", "--set", action="append",
                        dest="set_conf", default=[], help="设置dtv.conf文件")

    parser.add_argument("-c", "-config", "--config", action="store_true",
                        dest="show_conf", default=False, help="显示当前使用的dtv.conf文件路径")

    parser.add_argument("-r", "-root", "--root", action="store_true",
                        dest="show_root", default=False, help="显示当前的根节点路径")

    parser.add_argument("-z", "-hp", "--hidePath", "-short", "--short",
                        action="store_true", dest="use_short_path", default=False, help="按照最短路径显示")

    parser.add_argument("-f", "-find", "--find", action="store", type=parse_list,
                        dest="find_keyword", default="", help="搜索指定内容")

    parser.add_argument("-fre", "-findre", "--findre", action="store",
                        dest="findre_keyword", default="", help="正则的方式搜索指定内容")

    parser.add_argument("-x", "-flag", "--flag", action="append", choices=[
                        "re", "all", "cc", "ss"], dest="func_flag", default=[""], help="re:启用正则表达式搜索 all:在所有树中显示 cc:(care_case)按大小写敏感来搜索 ss:(show_stock)展示所属的名称")

    parser.add_argument("-d", "-debug", "--debug", action="store_true",
                        dest="debug", default=False, help="Debug模式运行")

    parser.add_argument("-no-color", "--no-color", action="store_true",
                        dest="no_color", default=False, help="禁用颜色显示")

    parser.add_argument("-ls","--linestyle","--linestype", action="store",
                        dest="linestyle", default="vscode", help="在搜索结果中展示行号的时候,使用+(vim)或者:(vscode)")

    parser.add_argument("-v", "--version", action="store_true",
                        dest="show_version", default=False, help="显示当前版本")

    parser.add_argument("--history", action="store_true",
                        dest="show_history", default=False, help="显示历史变更记录")

    parser.add_argument('-h', '--help', action='help',
                        default="==SUPPRESS==", help="显示帮助页")

    args, argv = parser.parse_known_args()

    return args, argv


def read_conf(conf_path, path_list, look_out, config_out=None):
    dt_file_list = []
    if conf_path.endswith(".conf"):
        global configLoading
        if conf_path not in configLoading:
            configLoading.append(conf_path)
        else:
            logger.error(["重复加载配置文件:", conf_path])
            return False
        if os.path.exists(conf_path):
            if showConfig:
                logger.error(["使用的配置文件:", conf_path])
                exit(0)
            logger.error(["使用的配置文件:", conf_path])
            with open(conf_path) as fd:
                dt_file_list = fd.readlines()
        elif look_out:
            conf_path_list = conf_path.split('/')
            for length in range(len(conf_path_list)):
                if length == 0:
                    continue
                dtv_path = '/'.join(conf_path_list[0:-length]) + "/dtv.conf"

                if os.path.exists(dtv_path):
                    return read_conf(dtv_path, path_list, False, config_out)
                else:
                    pass
        else:
            logger.error([conf_path, "配置文件不存在"])
            return False
    else:
        logger.error([conf_path, "不支持的配置文件!"])
        return False

    for dt_file in dt_file_list:
        dt_file = dt_file.replace("\n", "").replace(
            "\r", "").replace(" ", "").replace("\t", "")
        if not len(dt_file):
            continue
        if dt_file[0] == "#":
            continue
        if len(dt_file) <= 4:
            continue

        # 解析 key=value 形式的默认参数
        if '=' in dt_file:
            try:
                k, v = dt_file.split('=', 1)
                k = k.strip().lower()
                v = v.strip()
                if config_out is not None and k:
                    config_out[k] = v
                continue
            except Exception:
                pass

        if not os.path.exists(dt_file):
            '''
            兼容配置文件里的相对路径和绝对路径
            '''
            if dt_file.strip()[0] != "/":
                directory = os.path.dirname(conf_path)
                dt_file_new = os.path.join(directory, dt_file)
                if not os.path.exists(dt_file_new):
                    logger.error(["忽略无效设备树:", dt_file_new, conf_path])
                    continue
                else:
                    dt_file = dt_file_new
            else:
                logger.error(["忽略无效设备树:", dt_file])
                continue

        dt_file = os.path.abspath(dt_file)

        if dt_file in path_list:
            continue
        if dt_file.endswith(".dtsi"):
            path_list.append(dt_file)
        elif dt_file.endswith(".dtso"):
            path_list.append(dt_file)
        elif dt_file.endswith(".dts"):
            path_list.append(dt_file)
        elif dt_file.endswith(".conf"):
            if dt_file == conf_path:
                '''
                避免递归调用config文件
                '''
                logger.error(["避免递归调用:", dt_file])
                continue
            read_conf(dt_file, path_list, False, config_out)
        else:
            continue

    return len(path_list) > 0


def main():

    global findString
    global rootPath
    global hidePath
    global showAll
    global findWithRegex
    global noColor
    global showConfig
    global showRoot
    global findCareCase
    global showStock
    global linestyle

    sys.setrecursionlimit(100000)

    options, argv = parse_args()
    set_logger(options.debug)
    logger.debug([options, argv])

    showConfig = options.show_conf
    showRoot = options.show_root

    if options.show_history:
        logger.error("%s" % History)
        exit(0)

    if options.set_conf:
        conflist = []
        for tmp_path in options.set_conf:
            tmp_path = os.path.abspath(tmp_path)
            if os.path.isfile(tmp_path) and (tmp_path.endswith('.dts') or tmp_path.endswith('.dtsi') or tmp_path.endswith('.conf')):
                conflist.append(tmp_path)

        if len(conflist):
            with open('%s/dtv.conf' % (current_dir), 'w+', encoding='utf-8') as dtvfd:
                for line in conflist:
                    dtvfd.write("%s\n" % line)
            logger.error(["配置文件设置成功:", '%s/dtv.conf' % (current_dir)])
        else:
            logger.error(["输入的配置文件异常,请检查:", options.set_conf])

        exit(0)

    if options.show_version:
        logger.error(["当前版本:", Version])
        logger.error(ABOUT)
        exit(0)

    findWithRegex = "re" in options.func_flag
    findString = options.find_keyword

    if findWithRegex:
        """
        如果是正则表达式,则只支持一个搜索规则
        """
        findString = findString[0]

    if len(options.findre_keyword):
        findWithRegex = True
        findString = options.findre_keyword

    hidePath = options.use_short_path

    # 推迟颜色/显示相关选项到读取配置文件后再决定
    # noColor = options.no_color
    # showAll = "all" in options.func_flag
    # findCareCase = "cc" in options.func_flag
    # showStock = "ss" not in options.func_flag

    # linestyle = options.linestyle

    dt_file_list = []
    config_map = {}
    if argv:
        '''传参中指定配置文件或者根设备树'''
        for unknow_args in argv:
            logger.debug([unknow_args])
            file_path = os.path.abspath(unknow_args)
            logger.debug([file_path])
            if not os.path.exists(file_path):
                logger.error(["参数错误,无法解析参数[%s]" % unknow_args])
                exit(0)
            if unknow_args.endswith(".conf"):
                read_conf(file_path, dt_file_list, False, config_map)
            elif unknow_args.endswith('.dts') or unknow_args.endswith('.dtsi'):
                dt_file_list.append(unknow_args)
            else:
                logger.error(["忽略不支持的文件类型:", unknow_args])
    else:
        '''
        传参中没指定配置文件或者根设备树
        此时默认从脚本当前的位置查找dtv.conf,如果找不到,则逐级向外寻找
        '''
        file_path = os.path.abspath("./dtv.conf")
        read_conf(file_path, dt_file_list, True, config_map)

    if not len(dt_file_list):
        logger.error(["根节点选择失败,没找到有效的 配置文件 或 根节点"])
        exit(0)

    # 合并配置默认值与CLI传参（CLI优先）
    def conf_get_bool(*keys) -> Optional[bool]:
        for k in keys:
            v = config_map.get(k)
            b = parse_bool(v)
            if b is not None:
                return b
        return None

    # 计算各项显示选项
    cli_showAll = ("all" in options.func_flag)
    cli_findCareCase = ("cc" in options.func_flag)
    # 将 ss 视为“启用显示所属名称”开关
    cli_showStock = ("ss" in options.func_flag)
    cli_noColor = options.no_color

    cfg_showAll = conf_get_bool('showall', 'show_all')
    cfg_findCareCase = conf_get_bool('findcarecase', 'carecase', 'cc')
    cfg_showStock = conf_get_bool('showstock', 'show_stock', 'ss')
    cfg_noColor = conf_get_bool('nocolor', 'no_color')

    # 应用优先级：CLI > 配置 > 默认
    showAll = True if cli_showAll else (cfg_showAll if cfg_showAll is not None else False)
    findCareCase = True if cli_findCareCase else (cfg_findCareCase if cfg_findCareCase is not None else False)
    showStock = True if cli_showStock else (cfg_showStock if cfg_showStock is not None else False)
    noColor = True if cli_noColor else (cfg_noColor if cfg_noColor is not None else False)

    # 行号风格（配置在 CLI 未显式设置时生效）。CLI 默认是 "vscode"
    linestyle = options.linestyle
    cfg_linestyle = config_map.get('linestyle') or config_map.get('linestype')
    if cfg_linestyle and (options.linestyle == 'vscode' or not options.linestyle):
        linestyle = cfg_linestyle

    if showRoot:
        logger.error("当前使用的根节点为:")

    for file_path in dt_file_list:
        if showRoot:
            logger.error(file_path)
            continue

        rootPath = os.path.abspath(file_path)
        rootPath = os.path.dirname(rootPath)
        rootDeviceTree = DeviceTreeMap(file_path)
        ctx = rootDeviceTree.createTreeMap(1)
        rootDeviceTree.prune_deleted(ctx)
        rootDeviceTree.printMap()


if __name__ == '__main__':
    sys.exit(main())
